package com.dces.jx.billtest.dataimport.base;


import com.dces.jx.billtest.config.BulkInsertConfig;
import com.xdo.bulkinsert.BulkInsertException;
import com.xdo.bulkinsert.BulkInsertFactory;
import com.xdo.dataimport.base.ImportHandlerInterface;
import com.xdo.dataimport.model.ExcelImportDto;
import com.xdo.dataimport.utils.XdoImportLogger;
import com.xdo.domain.ResultObject;
import com.xdo.i.IBulkInsert;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static com.dces.jx.billtest.dataimport.constant.Constants.BATCH;


/**
 * @author zyzhu
 * @date 2021/08/31
 *
 * 抽象-导入数据执行器
 */
@Component
@Slf4j
public abstract class AbstractCacheImportHandler<T> implements ImportHandlerInterface {

    @Resource
    private BulkInsertConfig bulkInsertConfig;

    /**
     * 业务校验方法（内存校验）
     *
     * @param mapBasicParam    任务基础参数(当前任务的sid、taskCode、tradeCode、importType、insertUserOid、insertUserName)
     * @param mapBusinessParam 业务自定义参数(比如insertUser、插入表体时用到的表头id等都可以在这里传入,需在前端导入时确定需要用到的参数并传递)
     * @param mapObjectList    实体列表map，可根据xmlContent的sheet的id获取某类实体列表
     * @return 业务校验结果，head根据xmlContent的sheet的field获取，无需设置；当采用临时表校验方式时tempOwnerId必须设置，否则插入正确数据时无法进行操作
     */
    @Override
    public List<ExcelImportDto> checkData(Map<String, Object> mapBasicParam, Map<String, Object> mapBusinessParam, Map<String, List<Object>> mapObjectList) throws Exception {
        List<ExcelImportDto> excelImportDtoList = new ArrayList<>();
        // 根据配置文件的每个sheet的id获取特定sheet转化出来的实体列表，按配置顺序获取
        String sheetId = getSheetId();
        if (!mapObjectList.containsKey(sheetId)) {
            XdoImportLogger.log("导入数据序列化的实体列表集合中不存在" + sheetId);
            return null;
        }
        if (checkBusinessParam(mapBusinessParam)) {
            List<Object> objectList = mapObjectList.get(sheetId);
            // 实体数量
            int intObjectListSize = objectList.size();
            XdoImportLogger.log("共获取实体：" + intObjectListSize);
            // 初始化正确数据集合
            List<T> correctList = Collections.synchronizedList(new ArrayList<>(intObjectListSize));
            // 初始化错误数据集合
            List<T> wrongList = Collections.synchronizedList(new ArrayList<>(intObjectListSize));
            // 初始化主键集合
            Set<String> primaryKeys = Collections.synchronizedSet(new HashSet<>(intObjectListSize));
            // 批量分页校验数据(并行计算)
            List<Boolean> checkStatusList = IntStream.range(0, (intObjectListSize + BATCH - 1) / BATCH)
                    .mapToObj(i -> objectList.subList(i * BATCH, Math.min(BATCH * (i + 1), intObjectListSize)))
                    .parallel().map(list -> checkEachBatchData(list, correctList, wrongList, primaryKeys,
                            mapBasicParam, mapBusinessParam)
                    ).collect(Collectors.toList());
            // 清空主键数据集合，释放内存
            primaryKeys.clear();
            // 处理分页校验失败的情况
            for (Boolean checkStatus : checkStatusList) {
                if (!checkStatus) {
                    XdoImportLogger.log("校验失败!");
                    return null;
                }
            }
            ExcelImportDto<T> excelImportDto = new ExcelImportDto<>();
            XdoImportLogger.log("correct：" + correctList.size() + ";wrong：" + wrongList.size());
            excelImportDto.setCorrectList(correctList);
            excelImportDto.setWrongList(wrongList);
            excelImportDtoList.add(excelImportDto);
        } else {
            XdoImportLogger.log("部分自定义参数未成功获取(emsNo, cavMark)");
            return null;
        }
        return excelImportDtoList;
    }

    /**
     * 获取导入的页面id
     *
     * @return 返回excel中sheet名称
     */
    protected String getSheetId() {
        return getTaskCode();
    }

    /**
     * 每批次数据校验总方法
     *
     * @param objectList 导入数据源集合
     * @param correctList 正确数据集合
     * @param wrongList 错误数据集合
     * @param primaryKeys 主键集合
     * @param mapBasicParam 基础业务参数
     * @param mapBusinessParam 账册编号等业务参数
     * @return 校验成功true,失败false
     */
    protected abstract boolean checkEachBatchData(List<Object> objectList, List<T> correctList,
                                                  List<T> wrongList, Set<String> primaryKeys,
                                                  Map<String, Object> mapBasicParam, Map<String, Object> mapBusinessParam);

    /**
     * 业务持久化数据方法
     *
     * @param mapBasicParam    任务基础参数(当前任务的sid、taskCode、tradeCode、importType、insertUserOid、insertUserName)
     * @param mapBusinessParam 业务自定义参数(比如insertUser、插入表体时用到的表头id等都可以在这里传入,需在前端导入时确定需要用到的参数并传递)
     * @param mapObjectList    正确数据序列化的实体列表集合，根据导入配置文件(xmlContent)的每个sheet配置的id获取相应实体列表（采用内存校验时传递）
     * @param tempOwnerIdList  临时表处理批次Id，单sheet时取第一条（采用临时表校验时传递）
     * @return 业务持久化数据结果，导入服务根据返回值设置任务状态
     */
    @Override
    public ResultObject persistData(Map<String, Object> mapBasicParam, Map<String, Object> mapBusinessParam, Map<String, List<Object>> mapObjectList, List<String> tempOwnerIdList) throws Exception {
        ResultObject resultObject = ResultObject.createInstance(true, "新增成功");
        // 根据配置文件的每个sheet的id获取特定sheet转化出来的实体列表
        String strEntityId = getSheetId();
        if (!mapObjectList.containsKey(strEntityId)) {
            resultObject.setSuccess(false);
            resultObject.setMessage("正确数据序列化的实体列表集合中不存在" + strEntityId);
            return resultObject;
        }
        List<Object> objectList = mapObjectList.get(strEntityId);

        // 从自定义参数中获取需要的数据，此处是导入表体，需获取对应表头的id和当前操作者
        if (!checkBusinessParam(mapBusinessParam)) {
            resultObject.setSuccess(false);
            resultObject.setMessage("部分自定义参数未成功获取");
            return resultObject;
        }
        //遍历当前实体列表进行转换插入
        int intObjectListSize = objectList.size();
        List<T> beanDataList = Collections.synchronizedList(new ArrayList<>(intObjectListSize));
        XdoImportLogger.log("共获取实体：" + intObjectListSize);
        String tempOwnerId = UUID.randomUUID().toString();
        IntStream.range(0, (intObjectListSize + BATCH - 1) / BATCH)
                .mapToObj(i -> objectList.subList(i * BATCH, Math.min(BATCH * (i + 1), intObjectListSize)))
                .parallel().map(list -> setEachBatchData(list, mapBasicParam, mapBusinessParam, beanDataList, tempOwnerId)
        ).collect(Collectors.toList());
        XdoImportLogger.log("共获取插入实体：" + beanDataList.size() + ";执行快速入库操作");
        int intEffectCount = 0;
        String strErr = "";
        try {
            if (CollectionUtils.isNotEmpty(beanDataList)) {
                // 初始化快速入库实例（在config文件夹下新建OracleBulkConfig用于读取配置文件中的数据库连接配置）
                IBulkInsert iBulkInsert = BulkInsertFactory.createInstance(bulkInsertConfig);
                intEffectCount = iBulkInsert.fastImport(beanDataList);
            }
        } catch (BulkInsertException ex) {
            intEffectCount = ex.getCommitRowCount();
            strErr = "执行数据批量入库时,正式表已插入" + intEffectCount + "条，出错：" + ex.getMessage();
            XdoImportLogger.log(ex.getMessage());
        } catch (Exception ex) {
            strErr = "执行数据批量入库时,正式表已插入" + intEffectCount + "条，出错：" + ex.getMessage();
            XdoImportLogger.log(ex.getMessage());
        }
        if (StringUtils.isNotBlank(strErr)) {
            resultObject.setSuccess(false);
            resultObject.setMessage(strErr);
        }
        return resultObject;
    }

    // -------------------以下方法均为业务校验方法-------------------
    /**
     * 校验: 业务参数是否传入
     *
     * @param mapBusinessParam 业务参数
     * @return 正确反悔true，错误返回false
     */
    private boolean checkBusinessParam(Map<String, Object> mapBusinessParam) {
        return MapUtils.isNotEmpty(mapBusinessParam);
    }

    /**
     * 每批次数据封装总方法
     *
     * @param objectList 导入数据源集合
     * @param mapBasicParam 基础参数
     * @param mapBusinessParam 业务参数
     * @param beanDataList 入库数据集合
     * @param batchId 批次号
     * @return true:成功 false:失败
     */
    protected abstract Boolean setEachBatchData(List<Object> objectList, Map<String, Object> mapBasicParam,
                                     Map<String, Object> mapBusinessParam, List<T> beanDataList, String batchId);

}
